import CredentialsProvider from "next-auth/providers/credentials" import { getOrCreateSAMLUser, validateSAMLUserData } from '@/lib/users/saml-service' import { encode } from 'next-auth/jwt' import type { User } from 'next-auth' import type { SAMLUser } from './utils' import { debugLog, debugError, debugSuccess, debugProcess } from '@/lib/debug-utils' interface SAMLProviderOptions { id: string name: string idp: { sso_login_url: string sso_logout_url: string certificates: string[] } sp: { entity_id: string private_key: string certificate: string assert_endpoint: string } } export function SAMLProvider(options: SAMLProviderOptions) { return CredentialsProvider({ id: options.id, name: options.name, credentials: { user: { label: "User Data", type: "text" } }, async authorize(credentials) { debugLog('๐Ÿ” SAMLProvider.authorize called with credentials:', credentials); try { debugLog('๐Ÿ” Checking credentials.user:', { hasCredentials: !!credentials, hasUser: !!credentials?.user, userType: typeof credentials?.user, userValue: credentials?.user?.substring?.(0, 100) + '...' }); if (!credentials?.user) { debugError('No user data provided in credentials') return null } debugProcess('SAML Provider: Processing user data') // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ (UTF-8 ์ฒ˜๋ฆฌ ๊ฐœ์„ ) const userDataString = credentials.user debugLog('๐Ÿ”ค Raw user data string:', userDataString.substring(0, 200) + '...') let userData; try { userData = JSON.parse(userDataString); debugSuccess('JSON parsing successful:', userData); } catch (parseError) { debugError('JSON parsing failed:', parseError); debugError('Raw string that failed to parse:', userDataString); return null; } // ํŒŒ์‹ฑ๋œ ๋ฐ์ดํ„ฐ์˜ UTF-8 ํ™•์ธ debugLog('๐Ÿ”ค Parsed user data UTF-8 check:', { name: userData.name, nameLength: userData.name?.length, charCodes: userData.name ? [...userData.name].map(c => c.charCodeAt(0)) : [] }) if (!userData.id || !userData.email) { debugError('Invalid SAML user data:', userData) return null } debugSuccess('SAML Provider: User authenticated successfully', { id: userData.id, email: userData.email, name: userData.name }) // ๐Ÿ”ฅ SAML ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ debugProcess('Validating SAML user data structure...'); const isValidData = await validateSAMLUserData(userData) debugLog('Validation result:', isValidData); if (!isValidData) { debugError('Invalid SAML user data structure:', userData) return null } // ๐Ÿ”ฅ JIT (Just-In-Time) ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ debugProcess('Creating/getting SAML user from database...'); const userCreateData = { email: userData.email, name: userData.name, companyId: undefined, techCompanyId: undefined, domain: userData.domain }; debugLog('User create data:', userCreateData); const dbUser = await getOrCreateSAMLUser(userCreateData) debugLog('Database user result:', dbUser); if (!dbUser) { debugError('Failed to get or create SAML user') return null } // DB์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜ const userResult = { id: String(dbUser.id), // DB์˜ ์‹ค์ œ ID name: dbUser.name, // DB์˜ ์‹ค์ œ ์ด๋ฆ„ email: dbUser.email, // DB์˜ ์‹ค์ œ ์ด๋ฉ”์ผ companyId: dbUser.companyId, // DB์˜ ์‹ค์ œ ํšŒ์‚ฌ ID techCompanyId: dbUser.techCompanyId, // DB์˜ ์‹ค์ œ ๊ธฐ์ˆ ํšŒ์‚ฌ ID domain: dbUser.domain, // DB์˜ ์‹ค์ œ ๋„๋ฉ”์ธ imageUrl: dbUser.imageUrl, // DB์˜ ์‹ค์ œ ์ด๋ฏธ์ง€ URL } debugSuccess('SAML Provider: Returning user data to NextAuth:', userResult) return userResult } catch (error) { debugError('SAML Provider: Authentication failed', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, errorType: typeof error, credentials: credentials }); return null } } }) } // SAML ๋กœ๊ทธ์ธ URL ์ƒ์„ฑ ํ—ฌํผ ํ•จ์ˆ˜ export function getSAMLLoginUrl(options: SAMLProviderOptions): string { const params = new URLSearchParams({ SAMLRequest: 'placeholder', // ์‹ค์ œ๋กœ๋Š” createAuthnRequest()๋กœ ์ƒ์„ฑ RelayState: options.sp.assert_endpoint, }) return `${options.idp.sso_login_url}?${params.toString()}` } // SAML ์„ค์ • ๊ฒ€์ฆ export function validateSAMLOptions(options: SAMLProviderOptions): boolean { const required = [ options.idp.sso_login_url, options.sp.entity_id, options.sp.assert_endpoint ] return required.every(field => field && field.length > 0) } // SAMLProvider์˜ authorize ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•œ ํ—ฌํผ export async function authenticateSAMLUser(userData: SAMLUser) { debugLog('authenticateSAMLUser called with:', userData); try { // SAMLProvider ๋Œ€์‹  ์ง์ ‘ ๋กœ์ง ์‹คํ–‰ (Provider ๋ž˜ํผ ์—†์ด) debugProcess('SAML User Authentication: Processing user data') // ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ if (!userData.id || !userData.email) { debugError('Invalid SAML user data:', userData) return null } debugSuccess('SAML User data validated successfully', { id: userData.id, email: userData.email, name: userData.name }) // ๐Ÿ”ฅ SAML ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ debugLog('Validating SAML user data structure...'); const isValidData = await validateSAMLUserData(userData) debugLog('Validation result:', isValidData); if (!isValidData) { debugError('Invalid SAML user data structure:', userData) return null } // ๐Ÿ”ฅ JIT (Just-In-Time) ์‚ฌ์šฉ์ž ์ƒ์„ฑ ๋˜๋Š” ์กฐํšŒ debugLog('Creating/getting SAML user from database...'); const userCreateData = { email: userData.email, name: userData.name, companyId: undefined, techCompanyId: undefined, domain: userData.domain }; debugLog('User create data:', userCreateData); const dbUser = await getOrCreateSAMLUser(userCreateData) debugLog('Database user result:', dbUser); if (!dbUser) { debugError('Failed to get or create SAML user') return null } // DB์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ์‚ฌ์šฉ์ž ์ •๋ณด ๋ฐ˜ํ™˜ const userResult = { id: String(dbUser.id), // DB์˜ ์‹ค์ œ ID name: dbUser.name, // DB์˜ ์‹ค์ œ ์ด๋ฆ„ email: dbUser.email, // DB์˜ ์‹ค์ œ ์ด๋ฉ”์ผ companyId: dbUser.companyId, // DB์˜ ์‹ค์ œ ํšŒ์‚ฌ ID techCompanyId: dbUser.techCompanyId, // DB์˜ ์‹ค์ œ ๊ธฐ์ˆ ํšŒ์‚ฌ ID domain: dbUser.domain, // DB์˜ ์‹ค์ œ ๋„๋ฉ”์ธ imageUrl: dbUser.imageUrl, // DB์˜ ์‹ค์ œ ์ด๋ฏธ์ง€ URL } debugSuccess('SAML User Authentication completed:', userResult) return userResult; } catch (error) { debugError('authenticateSAMLUser error:', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, userData }); return null; } } // NextAuth JWT ํ† ํฐ ์ƒ์„ฑ ํ—ฌํผ export async function createNextAuthToken(user: User): Promise { const token = { id: user.id, email: user.email, name: user.name, companyId: user.companyId, techCompanyId: user.techCompanyId, domain: user.domain, imageUrl: user.imageUrl, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60) // 30์ผ }; const secret = process.env.NEXTAUTH_SECRET!; return await encode({ token, secret }); } // NextAuth ์„ธ์…˜ ์ฟ ํ‚ค ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ export function getSessionCookieName(): string { return process.env.NODE_ENV === 'production' ? '__Secure-next-auth.session-token' : 'next-auth.session-token'; }